﻿// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================
#region

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using SL.IdentityModel.Protocols.WSTrust;

#if SILVERLIGHT
using System.Net.Browser;
#endif

#endregion

namespace Microsoft.Xrm.Sdk.Client
{
	public class DocumentCompletedEventArgs : EventArgs
	{
		public Uri ServiceUri { get; set; }
		public XDocument Document { get; set; }
	}

	internal class LiveAuthenticationWebRequestState
	{
		public ClientCredentials Credentials { get; set; }
		public RequestedSecurityToken DeviceToken { get; set; }
		public String Policy { get; set; }
		public String Partner { get; set; }

		public Uri ServiceUri { get; set; }
		public HttpWebRequest Request { get; set; }
	}

	internal class LiveDeviceAuthenticationWebRequestState
	{
		public ClientCredentials Credentials { get; set; }

		public Uri ServiceUri { get; set; }
		public HttpWebRequest Request { get; set; }
	}

	public class LiveIDTokenManager
	{
		#region Templates

		private const string DeviceTokenTemplate =
			@"<?xml version=""1.0"" encoding=""UTF-8""?>
        <s:Envelope 
          xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" 
          xmlns:wsse=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"" 
          xmlns:wsp=""http://schemas.xmlsoap.org/ws/2004/09/policy"" 
          xmlns:wsu=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" 
          xmlns:wsa=""http://www.w3.org/2005/08/addressing"" 
          xmlns:wst=""http://schemas.xmlsoap.org/ws/2005/02/trust"">
           <s:Header>
            <wsa:Action s:mustUnderstand=""1"">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
            <wsa:To s:mustUnderstand=""1"">http://Passport.NET/tb</wsa:To>    
            <wsse:Security>
              <wsse:UsernameToken wsu:Id=""devicesoftware"">
                {0:userName}
                {1:password}
              </wsse:UsernameToken>
            </wsse:Security>
          </s:Header>
          <s:Body>
            <wst:RequestSecurityToken Id=""RST0"">
                 <wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
                 <wsp:AppliesTo>
                    <wsa:EndpointReference>
                       <wsa:Address>http://Passport.NET/tb</wsa:Address>
                    </wsa:EndpointReference>
                 </wsp:AppliesTo>
              </wst:RequestSecurityToken>
          </s:Body>
        </s:Envelope>
        ";

		private const string UserTokenTemplate =
			@"<?xml version=""1.0"" encoding=""UTF-8""?>
    <s:Envelope 
      xmlns:s=""http://www.w3.org/2003/05/soap-envelope"" 
      xmlns:wsse=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"" 
      xmlns:wsp=""http://schemas.xmlsoap.org/ws/2004/09/policy"" 
      xmlns:wsu=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"" 
      xmlns:wsa=""http://www.w3.org/2005/08/addressing"" 
      xmlns:wst=""http://schemas.xmlsoap.org/ws/2005/02/trust"">
       <s:Header>
        <wsa:Action s:mustUnderstand=""1"">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
        <wsa:To s:mustUnderstand=""1"">http://Passport.NET/tb</wsa:To>    
       <ps:AuthInfo Id=""PPAuthInfo"" xmlns:ps=""http://schemas.microsoft.com/LiveID/SoapServices/v1"">
             <ps:HostingApp>{0:clientId}</ps:HostingApp>
          </ps:AuthInfo>
          <wsse:Security>
             <wsse:UsernameToken wsu:Id=""user"">
                {1:userName}
                {2:password}
             </wsse:UsernameToken>
             <wsse:BinarySecurityToken ValueType=""urn:liveid:device"">
               {3:deviceTokenValue}
             </wsse:BinarySecurityToken>
          </wsse:Security>
      </s:Header>
      <s:Body>
        <wst:RequestSecurityToken Id=""RST0"">
             <wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
             <wsp:AppliesTo>
                <wsa:EndpointReference>
                   <wsa:Address>{4:partnerUrl}</wsa:Address>
                </wsa:EndpointReference>
             </wsp:AppliesTo>
             <wsp:PolicyReference URI=""{5:policy}""/>
          </wst:RequestSecurityToken>
      </s:Body>
    </s:Envelope>";

		private const string UserNameElement = "Username";
		private const string PasswordElement = "Password";
		private const string WSSENamespaceName = "wsse";
		private const string WSSENamespaceValue = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";

		#endregion

		#region Authenticate Device

		public IAsyncResult AuthenticateDevice(Uri authenticationEndpoint, ClientCredentials deviceCredentials)
		{
			LiveDeviceAuthenticationWebRequestState requestState = new LiveDeviceAuthenticationWebRequestState();
			requestState.Credentials = deviceCredentials;

#if SILVERLIGHT
			requestState.Request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(authenticationEndpoint);
#else
            requestState.Request = (HttpWebRequest)WebRequest.Create(authenticationEndpoint);
#endif			

			requestState.Request.Method = "POST";
			requestState.Request.ContentType = "application/soap+xml; charset=utf-8";

			//make the call
			return requestState.Request.BeginGetRequestStream(BeginGetDeviceTokenRequestStream, requestState);
		}

		private void BeginGetDeviceTokenRequestStream(IAsyncResult result)
		{
			LiveDeviceAuthenticationWebRequestState requestState =
				((LiveDeviceAuthenticationWebRequestState)result.AsyncState);
			using (Stream requestedStream = requestState.Request.EndGetRequestStream(result))
			{
				string soapEnvelope = string.Format(CultureInfo.InvariantCulture, DeviceTokenTemplate,
					BuildWSSEXmlNodeFragment(UserNameElement, requestState.Credentials.UserName.UserName), // 0
					BuildWSSEXmlNodeFragment(PasswordElement, requestState.Credentials.UserName.Password)); // 1

				using (StreamWriter streamWriter = new StreamWriter(requestedStream))
				{
					streamWriter.Write(soapEnvelope);
				}
			}
			//Now make the call to issue the device token
			requestState.Request.BeginGetResponse(BeginGetDeviceTokenResponse, requestState);
		}

		private void BeginGetDeviceTokenResponse(IAsyncResult result)
		{
			SignOnCompletedEventArgs args;
			try
			{
				LiveDeviceAuthenticationWebRequestState requestState =
					(LiveDeviceAuthenticationWebRequestState)result.AsyncState;
				WebResponse response = requestState.Request.EndGetResponse(result);
				using (Stream responseStream = response.GetResponseStream())
				{
					string responseString = String.Empty;
					using (StreamReader streamReader = new StreamReader(responseStream))
					{
						//parse the response message here                        
						responseString = streamReader.ReadToEnd();

						XNamespace encd = "http://www.w3.org/2001/04/xmlenc#";
						XElement root = XElement.Parse(responseString);

						XNamespace envelope = "http://www.w3.org/2003/05/soap-envelope";
						IEnumerable<XElement> faultElements = from cvls in root.Descendants(envelope + "Fault") select cvls;
						foreach (XElement faultElement in faultElements)
						{
							if (faultElement.Name.LocalName == "Fault")
							{
								FaultException exception = BuildFaultException(envelope, faultElement);
								args = new SignOnCompletedEventArgs(exception, false, null, null);
								OnDeviceSignOnCompleted(args);
								return;
							}
						}

						// Parse and return token
						args = new SignOnCompletedEventArgs(null, false, null, new RequestSecurityTokenResponse());

						IEnumerable<XElement> tokenElements = from cvls in root.Descendants(encd + "EncryptedData")
															  select cvls;
						foreach (XElement tokenElement in tokenElements)
						{
							if (tokenElement.Name.LocalName == "EncryptedData")
							{
								args.Result = new RequestSecurityTokenResponse
												  {
													  RequestedSecurityToken =
														  new RequestedSecurityToken(tokenElement.ToString())
												  };
							}
						}
						if (args.Result != null)
						{
							XNamespace wsTrust = "http://schemas.xmlsoap.org/ws/2005/02/trust";

							IEnumerable<XElement> lifeTimes = from cvls in root.Descendants(wsTrust + "Lifetime")
															  select cvls;

							XNamespace wsUtility =
								@"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";

							foreach (XElement lifeTime in lifeTimes)
							{
								IEnumerable<XElement> dates = from cvls in lifeTime.Descendants(wsUtility + "Created")
															  select cvls;
								foreach (XElement date in dates)
								{
									if (date.Name.LocalName == "Created")
									{
										args.Result.Created = Convert.ToDateTime(date.Value);
									}
								}

								IEnumerable<XElement> expires = from cvls in lifeTime.Descendants(wsUtility + "Expires")
																select cvls;
								foreach (XElement expire in expires)
								{
									if (expire.Name.LocalName == "Expires")
									{
										args.Result.Expires = Convert.ToDateTime(expire.Value);
									}
								}
							}
						}
					}
				}
			}
			catch (Exception ex) // We do want to catch all exceptions and propagate all the way back to the application
			{
				args = new SignOnCompletedEventArgs(ex, false, null, null);
			}
			OnDeviceSignOnCompleted(args);
		}

		private static FaultException BuildFaultException(XNamespace envelope, XElement faultElement)
		{
			FaultReason faultReason = null;
			FaultCode faultCode = null;

			var reasonElements = from r in faultElement.Elements(envelope + "Reason").Elements(envelope + "Text") select r;
			if (reasonElements.Count() > 0)
			{
				faultReason = new FaultReason(reasonElements.FirstOrDefault().Value);
			}

			var codeElements = from c in faultElement.Elements(envelope + "Code").Elements(envelope + "Subcode") select c;
			if (codeElements.Count() > 0)
			{
				faultCode = new FaultCode(codeElements.FirstOrDefault().Value);
			}

			return new FaultException(faultReason, faultCode, "http://www.w3.org/2005/08/addressing/soap/fault");
		}

		public event EventHandler<SignOnCompletedEventArgs> DeviceSignOnComplete;

		private void OnDeviceSignOnCompleted(SignOnCompletedEventArgs e)
		{
			if (null != DeviceSignOnComplete)
			{
				DeviceSignOnComplete(this, e);
			}
		}

		#endregion Authenticate Device

		#region Authenticate User

		public IAsyncResult Authenticate(Uri authenticationEndpoint, RequestedSecurityToken deviceToken,
										 ClientCredentials credentials, String partner, String policy)
		{
			LiveAuthenticationWebRequestState requestState = new LiveAuthenticationWebRequestState();
			requestState.ServiceUri = authenticationEndpoint;
			requestState.Credentials = credentials;
			requestState.DeviceToken = deviceToken;
			requestState.Partner = partner;
			requestState.Policy = policy;

#if SILVERLIGHT
			requestState.Request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(requestState.ServiceUri);
#else
            requestState.Request = (HttpWebRequest)WebRequest.Create(requestState.ServiceUri);
#endif			

			requestState.Request.Method = "POST";
			requestState.Request.ContentType = "application/soap+xml; charset=utf-8";

			//make the call
			return requestState.Request.BeginGetRequestStream(BeginGetUserAuthRequestStream, requestState);
		}

		private void BeginGetUserAuthRequestStream(IAsyncResult result)
		{
			LiveAuthenticationWebRequestState requestState = (LiveAuthenticationWebRequestState)result.AsyncState;
			using (Stream requestedStream = requestState.Request.EndGetRequestStream(result))
			{
				string soapEnvelope = string.Format(CultureInfo.InvariantCulture,
													UserTokenTemplate,
													Guid.NewGuid().ToString(), // 0
													BuildWSSEXmlNodeFragment(UserNameElement, requestState.Credentials.UserName.UserName), // 1
													BuildWSSEXmlNodeFragment(PasswordElement, requestState.Credentials.UserName.Password), // 2
													requestState.DeviceToken.RawToken, // 3
													requestState.Partner, // 4
													requestState.Policy); // 5

				using (StreamWriter streamWriter = new StreamWriter(requestedStream))
				{
					streamWriter.Write(soapEnvelope);
				}
			}
			requestState.Request.BeginGetResponse(BeginGetUserAuthResposne, requestState);
		}

		private string BuildWSSEXmlNodeFragment(string name, string value)
		{
			StringBuilder sb = new StringBuilder();
			using (XmlWriter wr = XmlWriter.Create(sb, new XmlWriterSettings() { OmitXmlDeclaration = true, ConformanceLevel = ConformanceLevel.Fragment }))
			{
				wr.WriteElementString(WSSENamespaceName, name, WSSENamespaceValue, value);
			}
			return sb.ToString();
		}

		private void BeginGetUserAuthResposne(IAsyncResult result)
		{
			SignOnCompletedEventArgs args;
			try
			{
				LiveAuthenticationWebRequestState requestState = (LiveAuthenticationWebRequestState)result.AsyncState;
				WebResponse response = requestState.Request.EndGetResponse(result);
				using (Stream responseStream = response.GetResponseStream())
				{
					RequestSecurityTokenResponse requestSecurityTokenResponse = new RequestSecurityTokenResponse();
					requestSecurityTokenResponse.AppliesTo = new EndpointAddress(requestState.Partner);
					string responseString = String.Empty;
					using (StreamReader streamReader = new StreamReader(responseStream))
					{
						//parse the response message here                        
						responseString = streamReader.ReadToEnd();

						XNamespace wsTrust = "http://schemas.xmlsoap.org/ws/2005/02/trust";
						XElement root = XElement.Parse(responseString);


						XNamespace envelope = "http://www.w3.org/2003/05/soap-envelope";
						IEnumerable<XElement> faultElements = from cvls in root.Descendants(envelope + "Fault") select cvls;
						foreach (XElement faultElement in faultElements)
						{
							if (faultElement.Name.LocalName == "Fault")
							{
								FaultException exception = BuildFaultException(envelope, faultElement);
								args = new SignOnCompletedEventArgs(exception, false, null, null);
								OnSignOnCompleted(args);
								return;
							}
						}

						// Parse and return token
						XNamespace encd = "http://www.w3.org/2001/04/xmlenc#";
						IEnumerable<XElement> tokenElements = from cvls in root.Descendants(encd + "EncryptedData")
															  select cvls;
						foreach (XElement tokenElement in tokenElements)
						{
							if (tokenElement.Name.LocalName == "EncryptedData")
							{
								requestSecurityTokenResponse.RequestedSecurityToken =
									new RequestedSecurityToken(tokenElement.ToString());
							}
						}

						IEnumerable<XElement> lifeTimes = from cvls in root.Descendants(wsTrust + "Lifetime")
														  select cvls;

						XNamespace wsUtility =
							@"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";

						foreach (XElement lifeTime in lifeTimes)
						{
							IEnumerable<XElement> dates = from cvls in lifeTime.Descendants(wsUtility + "Created")
														  select cvls;
							foreach (XElement date in dates)
							{
								if (date.Name.LocalName == "Created")
								{
									requestSecurityTokenResponse.Created = Convert.ToDateTime(date.Value);
								}
							}

							IEnumerable<XElement> expires = from cvls in lifeTime.Descendants(wsUtility + "Expires")
															select cvls;
							foreach (XElement expire in expires)
							{
								if (expire.Name.LocalName == "Expires")
								{
									requestSecurityTokenResponse.Expires = Convert.ToDateTime(expire.Value);
								}
							}
						}
						args = new SignOnCompletedEventArgs(null, false, null, requestSecurityTokenResponse);
					}
				}
			}
			catch (Exception ex) // We do want to catch all exceptions and propagate all the way back to the application
			{
				args = new SignOnCompletedEventArgs(ex, false, null, null);
			}
			OnSignOnCompleted(args);
		}

		public event EventHandler<SignOnCompletedEventArgs> SignOnComplete;

		private void OnSignOnCompleted(SignOnCompletedEventArgs e)
		{
			if (null != SignOnComplete)
			{
				SignOnComplete(this, e);
			}
		}

		#endregion Authenticate User
	}

	public class SignOnCompletedEventArgs : AsyncCompletedEventArgs
	{
		public SignOnCompletedEventArgs()
			: base(null, false, null)
		{
		}

		public SignOnCompletedEventArgs(Exception error, bool cancelled, object userState, RequestSecurityTokenResponse result)
			: base(error, cancelled, userState)
		{
			this.result = result;
		}

		RequestSecurityTokenResponse result;

		public RequestSecurityTokenResponse Result
		{
			get
			{
				base.RaiseExceptionIfNecessary();
				return this.result;
			}

			set
			{
				this.result = value;
			}
		}
	}

	public class AuthenticationCompletedEventArgs : AsyncCompletedEventArgs
	{
		public AuthenticationCompletedEventArgs()
			: base(null, false, null)
		{
		}

		public AuthenticationCompletedEventArgs(Exception error, bool cancelled, object userState, AuthenticationCredentials result)
			: base(error, cancelled, userState)
		{
			this.result = result;
		}

		AuthenticationCredentials result;

		public AuthenticationCredentials Result
		{
			get
			{
				base.RaiseExceptionIfNecessary();
				return this.result;
			}

			set
			{
				this.result = value;
			}
		}
	}

}
